﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FT.Architecture.NHibernate.SessionManagement;
using FT.Architecture.Queries;
using FT.Architecture.Utils.Reflection;
using NHibernate;

namespace FT.Architecture.NHibernate.Queries
{
    public class HqlExecutorService : IQueryExecutorService
    {
        private readonly ISessionProviderService sessionProvider;

        public HqlExecutorService(ISessionProviderService sessionProvider)
        {
            this.sessionProvider = sessionProvider;
        }

        public IEnumerable<T> Execute<T>(Query<T> query) where T : class
        {
            var session = sessionProvider.GetSession();
            var hqlQuery = CreateQuery("from {0} e", query, typeof(T), session, false);
            return hqlQuery.List<T>();
        }

        public long Count<T>(Query<T> query)
        {
            var session = sessionProvider.GetSession();
            var hqlQuery = CreateQuery("select count(*) from {0} e", query, typeof(T), session, true);
            return hqlQuery.UniqueResult<long>();
        }

        private static IQuery CreateQuery<T>(string initialQuery, Query<T> query, Type type, ISession session, bool ignoreFetches)
        {
            StringBuilder hql = new StringBuilder(string.Format(initialQuery, type.Name));

            if (!ignoreFetches)
            {
                hql.Append(GetFetchClause(query));
            }

            var criteria = GetAllCriteria(query);

            if (criteria.Count > 0)
            {
                hql.Append(" where");
            }

            int parameterIndex = 0;
            IList<HqlParameter> parameters = new List<HqlParameter>();
           
            hql.Append(" " + GetWhereClause(query, ref parameterIndex, parameters));

            if (query.Sort.Items.Count > 0)
            {
                hql.Append(string.Format(" order by "));

                var elements = from q in query.Sort.Items select (string.Format("e.{0} {1}", q.SortMember.GetCombinedProperty(), (q.SortOrder == SortOrder.Ascending ? "asc" : "desc")));
                string orderClause = elements.Aggregate((previous, next) => previous + ", " + next);
                hql.Append(orderClause);
            }

            var hqlQuery = session.CreateQuery(hql.ToString());

            // Setting the named parameters
            foreach (var parameter in parameters)
            {
                hqlQuery.SetParameter(parameter.Name, parameter.Value);
            }

            if (query.MaxResults.HasValue)
            {
                hqlQuery.SetMaxResults(query.MaxResults.Value);
            }

            if (query.FirstResult.HasValue)
            {
                hqlQuery.SetFirstResult(query.FirstResult.Value);
            }

            return hqlQuery;
        }

        private static string GetWhereClause<T>(Query<T> query, ref int parameterIndex, IList<HqlParameter> parameters)
        {
            string hql = string.Empty;
            var queryOperator = GetQueryOperator(query);

            foreach (var criterion in query.Criteria)
            {
                parameterIndex++;

                switch (criterion.CriteriaOperator)
                {
                    case CriteriaOperator.NotApplicable:
                    case CriteriaOperator.Equal:
                    case CriteriaOperator.LessThanOrEqual:
                    case CriteriaOperator.GreaterThanOrEqual:
                    case CriteriaOperator.LessThan:
                    case CriteriaOperator.GreaterThan:
                    case CriteriaOperator.NotEqual:
                    case CriteriaOperator.Like:
                        hql += string.Format(" e.{0} {1} :clause{2}", criterion.PropertyName, GetCriterionOperator(criterion), parameterIndex);
                        parameters.Add(new HqlParameter(parameterIndex, criterion.Value));
                        break;
                    case CriteriaOperator.SoundsLike:
                        hql += string.Format(" soundex(e.{0}) = soundex(:clause{1})", criterion.PropertyName, parameterIndex);
                        parameters.Add(new HqlParameter(parameterIndex, criterion.Value));
                        break;
                    case CriteriaOperator.IsNull:
                        hql += string.Format(" e.{0} is null", criterion.PropertyName);
                        break;
                    case CriteriaOperator.IsNotNull:
                        hql += string.Format(" e.{0} is not null", criterion.PropertyName);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                hql += string.Format(" {0}", queryOperator);
            }

            foreach (var subQuery in query.SubQueries)
            {
                string subQueryWhereClause = GetWhereClause(subQuery, ref parameterIndex, parameters);

                if (subQueryWhereClause != string.Empty)
                {
                    hql += string.Format(" ({0}) {1}", subQueryWhereClause, queryOperator);
                }
            }

            if (hql.EndsWith(string.Format(" {0}", queryOperator)))
                hql = hql.Remove(hql.Length - queryOperator.Length - 1, queryOperator.Length + 1);

            return hql;
        }

        private static string GetFetchClause<T>(Query<T> query)
        {
            string hql = "";

            foreach (var singleQuery in GetAllQueries(query))
            {
                foreach (var fetch in singleQuery.Fetches)
                {
                    hql += string.Format(" left join fetch e.{0}", fetch.PropertyName);
                }
            }

            return hql;
        }

        private static string GetQueryOperator<T>(Query<T> query)
        {
            switch (query.QueryOperator)
            {
                case QueryOperator.And:
                    return "and";
                case QueryOperator.Or:
                    return "or";
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private static string GetCriterionOperator<T>(Criterion<T> criterion)
        {
            switch (criterion.CriteriaOperator)
            {
                case CriteriaOperator.NotApplicable:
                    return string.Empty;
                case CriteriaOperator.Equal:
                    return "=";
                case CriteriaOperator.LessThanOrEqual:
                    return "<=";
                case CriteriaOperator.GreaterThanOrEqual:
                    return ">=";
                case CriteriaOperator.LessThan:
                    return "<";
                case CriteriaOperator.GreaterThan:
                    return ">";
                case CriteriaOperator.NotEqual:
                    return "<>";
                case CriteriaOperator.Like:
                    return "like";
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private static IList<Criterion<T>> GetAllCriteria<T>(Query<T> query)
        {
            List<Criterion<T>> all = new List<Criterion<T>>();

            all.AddRange(query.Criteria);

            foreach (var subQuery in query.SubQueries)
            {
                all.AddRange(GetAllCriteria(subQuery));
            }

            return all;
        }

        private static IEnumerable<Query<T>> GetAllQueries<T>(Query<T> query)
        {
            List<Query<T>> queries = new List<Query<T>> { query };

            foreach (var subQuery in query.SubQueries)
            {
                queries.AddRange(GetAllQueries(subQuery));
            }

            return queries;
        }

        private struct HqlParameter
        {
            public HqlParameter(int index, object value) : this()
            {
                Name = string.Format("clause{0}", index);
                Value = value;
            }

            public string Name { get; private set; }

            public object Value { get; private set; }
        }
    }
}